Fix: create per-task DI scope in ExecuteToolAsTaskAsync to prevent ObjectDisposedException#1433
Draft
Fix: create per-task DI scope in ExecuteToolAsTaskAsync to prevent ObjectDisposedException#1433
Conversation
… add regression test Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Copilot created this pull request from a session on behalf of
stephentoub
March 15, 2026 12:46
View session
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1430.
Root cause
When a tool with
TaskSupport = ToolTaskSupport.Required(orOptionalcalled with task metadata) is invoked,ExecuteToolAsTaskAsyncschedules the tool on a fire-and-forgetTask.Runand returns immediately withCallToolResult { Task = ... }. Control flows back toInvokeHandlerAsync, whosefinallyblock then disposes the per-requestIAsyncScope— before the background task has run at all.When the thread-pool eventually executes the background task and calls
tool.InvokeAsync(request, ...),AIFunctionMcpServerTool.InvokeAsyncwrapsrequest.Services(the now-disposed scope'sServiceProvider) in aRequestServiceProviderand uses it to resolve DI parameters. Since the scope is already disposed, this throwsObjectDisposedException— even for singleton registrations, because the call still goes through the disposed scope'sIServiceProvider.Fix
At the top of the
Task.Runbody inExecuteToolAsTaskAsync, create a freshIAsyncScopefrom the server's rootServices— exactly mirroring whatInvokeScopedAsyncdoes for synchronous tool calls — and assign itsServiceProvidertorequest.Services. The scope is disposed in thefinallyblock alongside the existing cleanup. The background task now has its own scope whose lifetime matches the tool's execution, not the HTTP request that scheduled it.The logic is directly analogous to how scoped services work for synchronous requests: each unit of work (request or background task) gets its own scope.
Changes
src/ModelContextProtocol.Core/Server/McpServerImpl.cs— create a freshAsyncServiceScopeat the start of theTask.Runlambda when_servicesScopePerRequestis true; replacerequest.Services; dispose infinally.tests/ModelContextProtocol.Tests/Server/McpServerTaskAugmentedValidationTests.cs— regression test: registers a scoped DI service (ITaskToolDiService), creates aTaskSupport = Requiredtool that resolves it as a method parameter, calls it with task metadata, polls to completion, and asserts the task reachedMcpTaskStatus.Completed(before the fix it would reachFailedfromObjectDisposedException).Security
No security impact. No new public API surface. No changes to authentication, authorization, or data handling.